import { Metric } from "web-vitals";
import {
  mapMetric,
  onVitals,
  getTimestamp,
  hashCode,
  isResourceTarget,
  dataTypes2BreadcrumbsType,
  dataCategory2BreadcrumbsCategory,
  sendByImg,
  getTraceDataType,
  getPerfLevel,
  getTraceDataLevel,
  uuid,
  generateUniqueId,
  getFingerprintId,
  interceptFetch,
  interceptXHR,
  initNetworkInterceptor,
  // enums
  TraceDataSeverity,
  BreadcrumbTypes,
  BreadcrumbsCategorys,
  TraceDataTypes,
  TraceTypes,
  TraceLevelType,
  SendType,
  getBrowserType,
  getClientType,
  sendByXHR,
  sendByFetch,
  sendBeacon,
  openIDB,
} from "@/core";
import {
  IBaseTrace,
  TracePerf,
  TraceAction,
  TraceBreadcrumbs,
  TraceData,
  TraceOptions,
  TraceTypeData,
  TraceDataResource,
  TraceDataLog,
  IBaseUserTrace,
} from "index";
import { parse } from "stacktrace-parser";

type UserInfo = Omit<IBaseUserTrace, "fpId">;

export class Trace implements IBaseTrace {
  // 等待上报数据队列
  #queue: TraceData[] = [];
  // 用户信息
  #userInfo: UserInfo = {};
  // 上报间隔
  private sendTimer: number = 100;
  // 上报定时器
  private timerId: unknown;
  // 性能日志数据
  private perfData: TracePerf = {
    id: "",
  };
  // 日志上报后端API
  private dsn: string = "";
  // 资源数据
  private resources: TraceDataResource[] = [];
  // 发送数据方式
  private sendType: SendType = SendType.img;

  // 是否开启debug
  public debug: boolean = false;
  // 记录用户行为
  public breadcrumb: TraceBreadcrumbs = [];
  // 浏览器信息
  public userAgent = navigator?.userAgent;
  // 浏览器类型
  public browserType = getBrowserType();
  // 客户端类型
  public clientType = getClientType();
  // fingerprintId
  public fpId: string | undefined = "";
  // 应用ID
  public appId: string = "";
  // 页面ID
  public pageId: string = "";
  // 最大存储用户行为
  public maxBreadcrumbs = 10;

  get traceQueue() {
    return this.#queue;
  }

  // 性能观察者
  public observer: PerformanceObserver;

  private constructor(options: TraceOptions) {
    this.debug = !!options.debug;
    this.maxBreadcrumbs = options.maxBreadcrumbs || 10;
    this.pageId = uuid();
    this.dsn = options.dsn || globalThis.location.origin;
    this.appId = options.appId;
    this.fpId = getFingerprintId("TraceCourse");
    this.perfData = {
      id: generateUniqueId(),
    };
    // 忽略其它业务逻辑
    this.observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        this.debug && console.log(`name    : ${entry.name}`);
        this.debug && console.log(`type    : ${entry.entryType}`);
        this.debug && console.log(`duration: ${entry.duration}`);
        if (entry.entryType === "resource") {
          this.handleObserverResource(entry as PerformanceResourceTiming);
        }
      });
    });
  }
  private handleObserverResource(entry: PerformanceResourceTiming) {
    let level = TraceDataSeverity.Info;
    if (entry.duration > 1000 && entry.duration < 1500) {
      level = TraceDataSeverity.Warning;
    } else if (entry.duration > 1500) {
      level = TraceDataSeverity.Error;
    }
    if (entry.duration > 1000) {
      let traceData = {
        dataId: hashCode(`${entry.entryType}-${entry.name}`),
        url: entry.name,
        name: `${entry.entryType}-duration-${entry.initiatorType}`,
        type: TraceDataTypes.PERF,
        level,
        message: `duration:${Math.round(entry.duration)}`,
        time: getTimestamp(),
      };
      this.resources.push(traceData);
      this.pushTraceQueue(this.setTraceData(traceData));
    }
  }
  private createPerfReport({
    perfBeforeSend,
    perfOnSend,
  }: {
    perfBeforeSend?: (data: TracePerf) => void;
    perfOnSend?: () => void;
  }) {
    const report = (metric: Metric) => {
      this.perfData = { ...this.perfData, ...mapMetric(metric) };
    };

    setTimeout(() => {
      const supportedEntryTypes = (PerformanceObserver && PerformanceObserver.supportedEntryTypes) || [];
      const isVisibilityChangeSupported = supportedEntryTypes.indexOf("layout-shift") !== -1;

      if (isVisibilityChangeSupported) {
        globalThis.addEventListener(
          "visibilitychange",
          async () => {
            if (document.visibilityState === "hidden") {
              perfBeforeSend?.(this.perfData);
              this.debug && console.log("hidden", this.perfData);
              await this.send(this.setTraceData(this.perfData));
              perfOnSend?.();
            }
          },
          { capture: true, once: true }
        );
      } else {
        globalThis.addEventListener(
          "pagehide",
          async () => {
            perfBeforeSend?.(this.perfData);
            this.debug && console.log("pagehide", this.perfData);
            await this.send(this.setTraceData(this.perfData));
            perfOnSend?.();
          },
          { capture: true, once: true }
        );
      }
    });
    return report;
  }
  private runSendAsync() {
    if (this.timerId) {
      return;
    }
    this.debug && console.log("[send queue start]");
    this.timerId = setInterval(() => {
      const data = this.#queue.shift();
      if (!data) {
        this.debug && console.log("[send queue end]");
        // @ts-ignore
        clearInterval(this.timerId);
        this.timerId = undefined;
        return;
      }
      this.send(data);
      this.debug && console.log("[data]: ", data);
    }, this.sendTimer) as unknown;
  }
  // 日志打印
  public log(log: TraceDataLog) {
    this.saveBreadcrumb(
      Object.assign(
        {
          name: "customer-log",
          level: log.level,
          type: dataTypes2BreadcrumbsType(log.type),
          category: dataCategory2BreadcrumbsCategory(log.type),
          message: log.message,
          time: getTimestamp(),
        },
        log
      )
    );
    this.debug && console.log(`log: ${JSON.stringify(log)}`);
    this.pushTraceQueue(this.setTraceData(log));
  }
  // 普通日志
  public info(message: string, tag?: string) {
    this.log({
      name: "customer-info",
      type: TraceDataTypes.LOG,
      level: TraceDataSeverity.Info,
      message,
      time: getTimestamp(),
      dataId: hashCode(`${message}|${tag || ""}`),
      tag,
    });
  }
  // 警告日志
  public warn(message: string, tag?: string) {
    this.log({
      name: "customer-warn",
      type: TraceDataTypes.LOG,
      level: TraceDataSeverity.Warning,
      message,
      time: getTimestamp(),
      dataId: hashCode(`${message}|${tag || ""}`),
      tag,
    });
  }
  // 错误日志
  public error(message: string, tag?: string) {
    this.log({
      name: "customer-error",
      type: TraceDataTypes.LOG,
      level: TraceDataSeverity.Error,
      message,
      time: getTimestamp(),
      dataId: hashCode(`${message}|${tag || ""}`),
      tag,
    });
  }
  public pageView(route: string, title?: string) {
    this.pushTraceQueue(
      this.setTraceData({
        name: "page-view",
        type: TraceDataTypes.ROUTE,
        level: TraceDataSeverity.Info,
        message: title || "",
        time: getTimestamp(),
        dataId: hashCode(`${route}|${title || ""}`),
        route: route,
      })
    );
  }
  public send(data: TraceData) {
    switch (this.sendType) {
      case SendType.beacon:
        return sendBeacon(this.dsn, data);
      case SendType.fetch:
        return sendByFetch(this.dsn, data);
      case SendType.xhr:
        return sendByXHR(this.dsn, data);
      default:
        return sendByImg(this.dsn, data);
    }
  }
  public setTraceData(data: TraceTypeData | TracePerf) {
    let type = TraceTypes.CONSOLE;
    let level = TraceLevelType.debug;
    let _data;
    let perf;
    if (!!(data as TraceTypeData).dataId) {
      type = getTraceDataType((data as TraceTypeData).type);
      level = getTraceDataLevel((data as TraceTypeData).level);
      _data = data as TraceTypeData;
    }
    if (!!(data as TracePerf).id) {
      type = TraceTypes.PERF;
      level = getPerfLevel(data as TracePerf);
      perf = data as TracePerf;
    }
    const traceData: TraceData = {
      type,
      level,
      createdAt: getTimestamp(),
      updatedAt: getTimestamp(),
      data: _data,
      perf,
      breadcrumbs: this.breadcrumb,
      traceId: uuid(),
      ua: this.userAgent,
      browserType: this.browserType,
      fpId: this.fpId!,
      uid: this.#userInfo?.uid,
      userName: this.#userInfo?.userName,
      email: this.#userInfo?.email,
      phone: this.#userInfo?.phone,
      appId: this.appId,
      clientType: this.clientType,
      url: document.URL,
      pid: this.pageId,
    };
    this.debug && console.log("[setTraceData]traceData: ", traceData);
    return traceData;
  }
  public saveError(event: ErrorEvent) {
    const target = event.target || event.srcElement;
    const isResTarget = isResourceTarget(target as HTMLElement);
    let traceData: TraceTypeData;
    let breadcrumbData: TraceAction;
    if (!isResTarget) {
      const errInfos = parse(event.error.stack)[0];
      traceData = {
        dataId: -1,
        name: "script-error",
        type: TraceDataTypes.JAVASCRIPT,
        level: TraceDataSeverity.Error,
        message: event.error.message,
        lineNumber: errInfos.lineNumber!,
        column: errInfos.column!,
        source: errInfos.file!,
        time: getTimestamp(),
      };
      breadcrumbData = {
        name: event.error.name,
        type: BreadcrumbTypes.CODE_ERROR,
        category: BreadcrumbsCategorys.Exception,
        level: TraceDataSeverity.Error,
        message: event.error.message,
        stack: event.error.stack,
        time: getTimestamp(),
      };
    } else {
      const nodeName = (target as HTMLElement).nodeName;
      const url = (target as HTMLElement).getAttribute("src") || (target as HTMLElement).getAttribute("href");
      const dataId = hashCode(`${nodeName}${event.message}${url}`);
      traceData = {
        dataId,
        name: "resource-load-error",
        level: TraceDataSeverity.Error,
        message: event.message,
        time: getTimestamp(),
        type: TraceDataTypes.RESOURCE,
      };
      breadcrumbData = {
        name: traceData.name,
        type: BreadcrumbTypes.RESOURCE,
        category: BreadcrumbsCategorys.Exception,
        level: TraceDataSeverity.Warning,
        message: event.message,
        time: getTimestamp(),
      };
    }
    this.resources.push(traceData);
    this.breadcrumb.push(breadcrumbData);
    this.pushTraceQueue(this.setTraceData(traceData));
  }
  public saveBreadcrumb(data: TraceAction) {
    this.breadcrumb.push(data);
    if (this.breadcrumb.length > this.maxBreadcrumbs) {
      this.breadcrumb.shift();
    }
  }
  public setUserInfo(userInfo: UserInfo) {
    this.#userInfo = userInfo;
  }
  public clearUserInfo() {
    this.#userInfo = {};
  }
  public clearQueue(): void {
    this.#queue = [];
  }
  public pushTraceQueue(data: TraceData) {
    this.#queue.push(data);
    this.runSendAsync();
  }
  // 捕获全局错误
  public onGlobalError(callback?: (e: Event) => {}) {
    window.addEventListener("error", (e) => {
      callback?.(e);
      this.saveError(e);
    });
  }
  // 捕获全局点击
  public onGlobalClick(callback?: (bc: TraceAction) => {}) {
    window.addEventListener("mousedown", (e) => {
      let target = e.target as HTMLElement;
      let paths: string[] = [];
      while (target.nodeName !== "HTML") {
        if (target.id) {
          paths.push(`#${target.id}`);
        } else if (target.classList?.length) {
          paths.push(`.${Array.from(target.classList).join(".")}`);
        } else {
          paths.push(target.nodeName);
        }
        target = target.parentNode as HTMLElement;
      }
      let pathStr = "";
      for (let i = paths.length - 1; i >= 0; i--) {
        pathStr += paths[i] + (i > 0 ? ">" : "");
      }
      const bc: TraceAction = {
        name: "click",
        level: TraceDataSeverity.Normal,
        type: BreadcrumbTypes.CLICK,
        category: BreadcrumbsCategorys.User,
        message: pathStr,
        time: getTimestamp(),
      };
      callback?.(bc);
      this.saveBreadcrumb(bc);
    });
  }
  // 初始化 SDK
  public static init(options: TraceOptions): IBaseTrace {
    const traceSdk = new Trace(options);
    traceSdk.observer.observe({
      entryTypes: ["resource"],
    });
    // 忽略部分逻辑
    globalThis.$trace = traceSdk;

    globalThis.fetch = interceptFetch(initNetworkInterceptor(traceSdk, "fetch"));
    // @ts-ignore
    globalThis.XMLHttpRequest = interceptXHR(initNetworkInterceptor(traceSdk, "xhr"));
    // 监听页面性能
    onVitals(
      traceSdk.createPerfReport({
        perfOnSend: options.perfOnSend,
        perfBeforeSend: options.perfBeforeSend,
      })
    );

    // 页面被关闭的时候存储在本地
    if (globalThis.onbeforeunload) {
      globalThis.onbeforeunload = async (e) => {
        try {
          const db = await openIDB();
          const store = db.transaction("logs", "readwrite").objectStore("logs");
          for (const item of traceSdk.traceQueue) {
            store.add(item);
          }
        } catch (error) {
          console.error(error);
        }
      };
    }

    openIDB().then((db) => {
      const transaction = db.transaction("logs", "readwrite");
      const request = transaction.objectStore("logs").getAll();
      request.onsuccess = (e) => {
        const datas = (e.target as IDBRequest).result as TraceData[];
        for (const data of datas) {
          traceSdk.pushTraceQueue(data);
        }
        db.transaction("logs", "readwrite").objectStore("logs").clear();
      };
    });

    return traceSdk as IBaseTrace;
  }
}
